HW Exam DevOps¶

Зорин Дмитрий Игоревич

Т.к. описание задания и критериев расходились по требованиям, то работа была выполнена в 2 этапа:

  1. Выполнено задание в ноутбуке с ADR и нагрузочным тестированием.
    • Папка: https://drive.google.com/drive/folders/1HsGTTh8FxeQVF_hIj-Y6Js1jM9cdzpuz?usp=sharing
    • Ноутбук: https://drive.google.com/file/d/1hQLZSf2ezD-xtCljlPXHvwkSgOhZEGQO/view?usp=sharing
  2. Создан отдельный проект с микросервисом, API, ADR, MLflow, Terraform (YC), OpenTofu, Github Actions, YC serverless container, Locust report и выводами.
    • YC: https://bbasqrbl76vu9ukarbc4.containers.yandexcloud.net/
    • Github: https://github.com/IDZorin/devops-exam

Описание¶

Критерий 1. Проектирование сервиса¶

Сервис реализован на FastAPI, эндпоинты заданы контрактами Pydantic:

  • GET /health (быстрый healthcheck),
  • GET /healthz (health с проверкой загрузки модели),
  • POST /predict (тело IrisFeatures с 4 признаками, ответ — predicted_class и probabilities).

Эндпоинты описаны в app/main.py, Swagger /docs доступен из контейнера; модель берётся из models/rf_iris_model.pkl, путь задаётся через MODEL_PATH.

Критерий 2. Анализ подходов к разделению монолита¶

Логические компоненты в одном репозитории:

  • API-сервис (FastAPI) в Docker-образе — принимает health/predict.
  • ML-пайплайн (src/train.py) — обучает RandomForest на Iris, сохраняет модель и отчёты (MLflow, Evidently, Deepchecks).
  • Инфраструктура — Docker/Docker Compose для локального запуска и Terraform для serverless-контейнера YC; переменные вынесены в tfvars.
  • Тестирование — Locust (locustfile.py) бьёт по /health и /predict.
  • Связи: пайплайн генерирует модель → API её грузит; Terraform/Compose разворачивают API; Locust нагрузочно тестирует API.

Критерий 3. Программное создание инфраструктуры¶

Использован Terraform: конфиги в terraform/*.tf поднимают Yandex Container Registry, репозиторий и serverless container с образом fastapi-sls:v3, переменная окружения PORT=8080 задаётся в блоке image. Переменные описаны в variables.tf, пример значений — terraform.tfvars.example; запуск: terraform init, terraform apply (tfvars с секретами держатся локально). Это обеспечивает развёртывание инфраструктуры с нуля.

Критерий 4. ML-пайплайн¶

Отчёт Evidently по train/test (Iris) показал отсутствие дрейфа: 0 из 5 колонок детектированы как дрейфующие, p-value для числовых признаков > 0.5, категориальный target без новых/пропавших категорий. Вывод: текущий тестовый срез статистически совпадает с обучающим, значимых сдвигов распределений нет; модель может работать стабильно для этой выборки.

Критерий 5. Принятие архитектурных решений¶

ADR файл создан. Выбран контейнеризированный FastAPI сервис с отдельным ML‑пайплайном и IaC (Terraform/Compose), потому что он обеспечивает воспроизводимость, чёткие контракты и переносимость между локальной средой, CI и облаком, несмотря на более высокую начальную сложность и необходимость управлять секретами.

Критерий 6. Итоговое оформление¶

Сервис отвечает корректно, но при такой нагрузке упирается в лимиты serverless. Для снижения 429 можно либо уменьшить rps, либо увеличить ресурсы/параметры concurrency/память, либо прогревать.

ADR¶

image.png

Local test¶

Mlflow¶

poetry run python src/train.py

image.png

image.png

API¶

poetry run uvicorn app.main:app --host 0.0.0.0 --port 8000

image.png

Evidently¶

In [ ]:
from IPython.display import IFrame, HTML
from pathlib import Path

nb_dir = Path.cwd()
report_path = (nb_dir / ".." / "reports" / "evidently_data_drift.html").resolve()
HTML(report_path.read_text(encoding="utf-8"))
Out[ ]:

Loading...

The Kernel crashed while executing code in the current cell or a previous cell. 

Please review the code in the cell(s) to identify a possible cause of the failure. 

Click <a href='https://aka.ms/vscodeJupyterKernelCrash'>here</a> for more info. 

View Jupyter <a href='command:jupyter.viewOutput'>log</a> for further details.

Docker¶

docker compose up --build -d

image.png

curl http://localhost:8000/health

StatusCode        : 200
StatusDescription : OK
Content           : {"status":"ok"}
RawContent        : HTTP/1.1 200 OK
                    Content-Length: 15
                    Content-Type: application/json
                    Date: Sat, 29 Nov 2025 19:45:53 GMT
                    Server: uvicorn

                    {"status":"ok"}
Forms             : {}
Headers           : {[Content-Length, 15], [Content-Type, application/json], [Date, Sat, 29 Nov 2025 19:45:53 GMT], [Server, uvicorn]}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 15

poetry run locust -f locustfile.py --headless --host=http://localhost:8000 -u 20 -r 2 -t 1m --html=reports/locust_report.html

In [1]:
from IPython.display import IFrame, HTML
from pathlib import Path

nb_dir = Path.cwd()
report_path = (nb_dir / ".." / "reports" / "locust_report.html").resolve()
HTML(report_path.read_text(encoding="utf-8"))
Out[1]:
Locust

image.png

Tofu¶

In [ ]:
cd tofu
tofu init
tofu validate

image.png

Github¶

image.png

Cloud: Yandex Container Registry¶

init¶

Авторизация и настройка Yandex Cloud yc init

yc container registry configure-docker

Terraform¶

In [ ]:
# перезаписать образ и пушить (т.к. использовался в HW4 с тегом v1)
docker build -t cr.yandex/crplljbgcj6qsfgsn2ln/fastapi-sls:v2 .
docker push cr.yandex/crplljbgcj6qsfgsn2ln/fastapi-sls:v2


cd terraform
terraform init



# Применение изменений
terraform apply

terraform plan

# Проверка работы
yc serverless container revision list --container-id <id>

# проверка health
curl.exe -v https://bbasqrbl76vu9ukarbc4.containers.yandexcloud.net/healthz

image.png

image.png

Locust¶

poetry run locust -f locustfile.py --headless --host=https://bbasqrbl76vu9ukarbc4.containers.yandexcloud.net -u 20 -r 2 -t 1m --html=reports/locust_report_cloud.html

In [10]:
from IPython.display import IFrame, HTML
from pathlib import Path

nb_dir = Path.cwd()
report_path = (nb_dir / ".." / "reports" / "locust_report_cloud.html").resolve()
HTML(report_path.read_text(encoding="utf-8"))
Out[10]:
Locust
In [2]:
from IPython.display import IFrame, HTML
from pathlib import Path

nb_dir = Path.cwd()
report_path = (nb_dir / ".." / "reports" / "exam_{Zorin}_test_report_100_10_60.html").resolve()
HTML(report_path.read_text(encoding="utf-8"))
Out[2]:
Locust

image.png

Выводы¶

  • Локально сервис очень быстрый: /health среднее ~6 мс, P95 ~15 мс; /predict среднее ~112 мс, P95 ~130 мс, хвост до ~1.4 с. RPS ~9.4, ошибок 0%.
  • В облаке задержки выше из‑за сетевых и холодных стартов: /health среднее ~211 мс (P95 ~650 мс, хвост до 5 s), /predict среднее ~280 мс (P95 ~700 мс, хвост до 7.7 s). RPS ~8.3, ошибок 0%.
  • Соотношение 75% health / 25% predict по сценарию, ошибок нет = стабильность под нагрузкой подтверждена.
  • Хвосты (P99) до нескольких секунд в облаке: холодные старты + ограниченные ресурсы (256 MB). Можно улучшить, увеличив память или прогрев.
  • При 100 юзерах/10 rps спавне (~40 rps суммарно) cloud: P50 ~180–190 мс, P95 ~0.5–0.6 с, хвосты до 9–12 с; 1.67 % 429 (Too Many Requests) на /health и /predict — лимиты serverless.

Вывод: Сервис отвечает корректно, но при такой нагрузке упирается в лимиты serverless. Для снижения 429 можно либо уменьшить rps, либо увеличить ресурсы/параметры concurrency/память, либо прогревать.